using System;
using System.Collections.Generic;
using System.Text;
using System.Linq;
using FriendsNearby.Services.Entities;
using InSTEDD.Labs.Geo;
using Microsoft.Practices.EnterpriseLibrary.PolicyInjection;
using Microsoft.Practices.EnterpriseLibrary.Logging;

namespace FriendsNearby.Services
{
    public class PeopleNearbyService : IPeopleNearbyService
    {
        IServiceProvider serviceProvider;

        public PeopleNearbyService(IServiceProvider serviceProvider)
        {
            this.serviceProvider = serviceProvider;
        }

        ILocationService LocationService
        {
            get { return serviceProvider.GetService<ILocationService>(); }
        }

        IUsersService UsersService
        {
            get { return serviceProvider.GetService<IUsersService>(); }
        }

        ISocialNetworkService SocialNetworkService
        {
            get { return serviceProvider.GetService<ISocialNetworkService>(); }
        }

        #region IPeopleNearbyService Members

        public IList<PersonNearby> GetPeopleNearby(string userId)
        {
            UserInfo info = LocationService.GetCurrentLocation(userId);

            return GetPeopleNearby(info, null);
        }

        public IList<PersonNearby> GetAllPeople(string userId)
        {
            UserInfo info = LocationService.GetCurrentLocation(userId);

            return InternalGetPeopleNearby(info, null, DateTime.Now, true);
        }

        private IList<PersonNearby> GetPeopleNearby(string userId, DateTime pointInTime)
        {
            //By definition, none was near none in the beginning of the beginnings
            if (pointInTime == DateTime.MinValue)
                return default(IList<PersonNearby>);

            UserInfo info = LocationService.GetLocationAt(userId, pointInTime);

            return GetPeopleNearby(info, pointInTime);
        }

        public IEnumerable<PersonNearby> GetAllFriends(string userId)
        {
            UserInfo info = LocationService.GetCurrentLocation(userId);

            return InternalGetPeopleNearby(info, null, null, false);
        }
        
        public IList<string> GetNotificationsForFacebookUser(string facebookID)
        {
            try
            {
                IList<PersonNearby> friendsNearbyNow = GetPeopleNearby(facebookID);
                Logger.Write(string.Format("FacebookID:{0} has {1} friends nearby now", facebookID, friendsNearbyNow.Count));

                IList<PersonNearby> friendsNearbyBefore = default(IList<PersonNearby>); 
                
                DateTime lastNotification = LocationService.GetLastNotificationDate(facebookID);
                //if this is not the first notification to the user                
                if (lastNotification != DateTime.MinValue)
                {
                    friendsNearbyBefore = GetPeopleNearby(facebookID, lastNotification);
                    Logger.Write(string.Format("FacebookID:{0} had {1} friends nearby at the time of the last notification: {2}",
                                                facebookID, friendsNearbyBefore.Count, lastNotification));
                                      
                }

                return NewFriendNearbyStrategy.GetNewFriendsNearby(friendsNearbyBefore, friendsNearbyNow); 
            }
            catch (Exception ex)
            {
                Logger.Write(string.Format("Exception thrown in GetNotificationsForFacebookUser:{0}",
                                            ex.ToString()));

                return default(IList<string>);
            }

        }

        #endregion

        private List<PersonNearby> GetPeopleNearby(UserInfo info, DateTime? pointInTime)
        {
            if (info == null || info.Location == null)
            {
                return new List<PersonNearby>();
            }

            UserPreference preferences = UsersService.GetUserPreferences(info.FacebookUserId);

            return InternalGetPeopleNearby(info, (double)preferences.NearbyRadiusKM, pointInTime, true);
        }

        /// <summary>
        /// List people nearby the user.
        /// Nearby means less or equal than <paramref name="distance"/> (if not null).
        /// <paramref name="includeFriendsOfFriends" /> specify if friends of friends should be computed
        /// </summary>
        /// <param name="userInfo"></param>
        /// <param name="distance"></param>
        /// <param name="pointInTime"></param>
        /// <param name="includeFriendsOfFriends"></param>
        /// <returns></returns>
        private List<PersonNearby> InternalGetPeopleNearby(UserInfo userInfo, double? distance, DateTime? pointInTime, bool includeFriendsOfFriends)
        {
            List<PersonNearby> result = new List<PersonNearby>();

            IEnumerable<string> users;

            //  Add Friends
            users = SocialNetworkService.GetFriends(userInfo.FacebookUserId);
            result.AddRange(CreatePersonNearbies(userInfo, users, PersonNearbyRelation.Friend, distance, pointInTime));

            if (includeFriendsOfFriends)
            {
                //  Add Friends of Friends
                users = SocialNetworkService.GetFriendsOfFriends(userInfo.FacebookUserId);
                result.AddRange(CreatePersonNearbies(userInfo, users, PersonNearbyRelation.FriendOfAFriend, distance, pointInTime));
            }

            // We have to sort again.
            // TODO optimize with another algorithm with merging.
            // Sort by distance
            #region sort by distance

            SortByDistanceAndFacebookUserId(result,
                x => x.Distance,
                x => x.FacebookUserID);
            #endregion

            return result;
        }

        private IEnumerable<PersonNearby> CreatePersonNearbies(UserInfo userInfo, IEnumerable<string> users, PersonNearbyRelation relation, double? distance, DateTime? pointInTime)
        {
            List<PersonNearby> personsNearby = new List<PersonNearby>();
            PersonNearbyBuilder personNearbyBuilder = new PersonNearbyBuilder("Unkown {0}");

            // users must be sorted before going to personNearbyBuilder and get numbered and Unkown {0}
            // that way, the name assigned is the same, no matter the distance.
            List<UserInfo> friendsInfo = new List<UserInfo>(GetUsersNearby(userInfo.Location, users, distance, pointInTime));
            SortByDistanceAndFacebookUserId(friendsInfo,
                x => userInfo.Location == null ? null : (double?)LatLong.DistanceInKm(userInfo.Location, x.Location),
                x => x.FacebookUserId);

            foreach (UserInfo friendInfo in friendsInfo)
            {
                try
                {
                    UserPreference preferences = UsersService.GetUserPreferences(friendInfo.FacebookUserId);
                    string friendDisplayName;

                    friendDisplayName = SocialNetworkService.GetUserDisplayName(userInfo.FacebookUserId, friendInfo.FacebookUserId);

                    PersonNearby person = personNearbyBuilder.CreateFriendNearby(userInfo, friendInfo, friendDisplayName, preferences, relation);

                    if (person != null)
                        personsNearby.Add(person);
                }
                catch (Exception e)
                {
                    LogEntry logEntry = new LogEntry();
                    logEntry.Message = e.ToString();
                    logEntry.Categories.Add("Oddities");
                    logEntry.EventId = LogEventIds.UnableToCreatePersonNearby;

                    Logger.Write(logEntry);
                }
            }
            return personsNearby;
        }

        private void SortByDistanceAndFacebookUserId<T>(List<T> items, Func<T, double?> distance, Func<T, string> userId)
        {
            items.Sort(
                (item1, item2) =>
                {
                    var distance1 = distance(item1).GetValueOrDefault(double.MaxValue);
                    var distance2 = distance(item2).GetValueOrDefault(double.MaxValue);
                    if (distance1 == distance2)
                        return userId(item1).CompareTo(userId(item2));
                    else
                        return distance1.CompareTo(distance2);
                });
        }

        private IEnumerable<UserInfo> GetUsersNearby(LatLong myLocation, IEnumerable<string> users, double? distance, DateTime? pointInTime)
        {
            foreach (string user in users)
            {
                UserInfo info;

                if(pointInTime.HasValue)
                    info = LocationService.GetLocationAt(user, pointInTime.Value);
                else
                    info = LocationService.GetCurrentLocation(user);

                if (info != null && info.Location != null && IsNear(myLocation, info.Location, distance)) 
                    yield return info;
            }
        }

        private static bool IsNear(LatLong fromLocation, LatLong toLocation, double? distance)
        {
            return (distance == null || LatLong.DistanceInKm(fromLocation, toLocation) <= distance);
        }
    }
}
